package autorization;
// check repository === TEST COMMENT
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * The authorisation table allows to make a data storage of the users registered in system
 * @author Volk ID = 1001
 * @version 0.1 Добавлен базовый функционал
 * @version 0.2 Проведена интеграция с системой ролей, добавлены сервисы для работы с ролями пользователя
 */
public class AutorizationTable implements IAutorization{

  //--------------------------- Inner Methoods --------------------------------
    /**
     * To set path to autorization file
     * @param path - path to file. if it equal null-valuee, then autorization information puts into
     * default project folder with name quual class constant "defaultFile". If entry folder is not exist
     * then this folder will create. if entry file is not exits then this file will create. If
     * entry file is exist then information from the file load to memory.
     * @throws java.net.URISyntaxException - syntax error in path
     * @throws java.io.IOException - input-output error
     * @throws autorization.FormatException The file format does not correspond to a format supported by the given version of the program
     */
    private void _InitPath(String path) throws URISyntaxException, IOException, FormatException{
      if(path == null){
       path = ".";
      }
      File file = new File(path);
      //To check up correct format of path
      URI uri = new URI(file.toURI().toString());
      //if path is directory then concatanation to path the default file name
      if(file.isDirectory()){
        if(!file.exists()) file.mkdirs();
        path = file.getPath() + File.separator + defaultFile;
        file = new File(path);
      }
      //If file exist then exit else to create new file
      this.path = path;
      if(!file.exists()){
        file.createNewFile();
        _Export();
      }
       else{
        //Call of import methood
        Import(path,IAutorization.IMPORT_CURRENT);
       }    
    }

    private boolean isExistFile = true; //If there was an input-output error in the course of reading from a file or writing this variable accepts  false-value.
    /**
     * Export autorization information to autorization file
     * @return true - in case if operation carried out, else false;
     */
    private boolean _Export(){
       Thread thread = new Thread(new Runnable() {
       public void run() {
           writeLockFile.lock();
           try {

               DataOutputStream stream = new DataOutputStream(new FileOutputStream(getPath()));
               stream.writeUTF(AutorizationTable.format);
               stream.writeInt(getSize());
               IAutorizationIterator iter = getIterator();

               iter.begin();
               while(iter.Next()){
                 
                 String login = " ";
                 String password = " ";

                 login = (String) iter.getLogin();
                 password = iter.getPassword();
                 long role_id = iter.getRole();

                 try {
                   long lID = _getID(login, password);
                   stream.writeLong(lID);
                   stream.writeUTF(login);
                   stream.writeUTF(password);
                   stream.writeLong(role_id);

                   } catch (RegistrationFailed e) {
                   } //To pass to next element
               }
               stream.close();
           } catch (FileNotFoundException e) {
               isExistFile = false;
           } catch (IOException ex) {
               isExistFile = false;
           }
           writeLockFile.unlock();
       }
      });

      thread.setPriority(Thread.NORM_PRIORITY-1);
      thread.start();

       if(!isExistFile){
        isExistFile = true;
        return false;
      }else return true;
    }
/**
 * returns hashed password using md5 algorithm
 * @param password - user's password
 * @return String with hashed password using md5 algoritht. if returned null-valuet then MD5 is
 * not supported current system
 */
    String _getPasswordMD5(String password){
      MessageDigest md;
        String result = null;
        try {
            md = MessageDigest.getInstance("MD5");
            md.update(password.getBytes(), 0, password.length());
            result = new BigInteger(1, md.digest()).toString(16);
            //System.out.println(password + "  :  " + result);
        } catch (NoSuchAlgorithmException ex) {
            System.out.println("MD5 is not supported current system");
        }
        return result;
    }

    long _getID(String login, String Password) throws RegistrationFailed{
      try{
       RecordThreadTable record = table.get(login);
         if(record == null) throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
         if(!record.get(PASSWORD).equals(Password))
         throw new RegistrationFailed(RegistrationFailed.PASSWORD_ERROR);
         return ((LongComparable)record.get(ID)).value;
      }catch(Exception exc){
       throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
      }
    }
  //--------------------------- Class constants -------------------------------
    private final int FIELD_COUNT = 3; //Fields Count in a Record
    private final int ROLE = 2; //идентификатор роли
    private final int PASSWORD = 0; // identifier of field-password
    private final int ID = 1; //
    static public final String format = "GFDAF 2.0"; //Format of saving authorization table
    static public final String defaultFile = "GFDAutorizationDefault.txt"; //default name autorization file
  //---------------------------------------------------------------------------
  //-------------------------- Class attributes  ---------------------------------
    private long IDCounter = 0; // counter of unicue users values
    private int size = 0; //count of registered user
    private ThreadTable<String> table =  new ThreadTable<String>(); //the Table for comparison of a login to data and ID
    private ConcurrentHashMap<LongComparable,String> mapID = new ConcurrentHashMap<LongComparable,String>();  //The table for comparison ID to a login
    private String path; //the Path to a file in which data on authorisation are stored
    private Lock readLockFile; //Locking on reading
    private Lock writeLockFile; //Locking on writing
  //---------------------------------------------------------------------------
  //--------------------------Interface Methods ------------------------------
     /**
     * constructor
     * @param path - a way on which the authorisation table should be created
     * @throws java.net.URISyntaxException - syntax error in path
     * @throws java.io.IOException - input-output error
     * @throws autorization.FormatException The file format does not correspond to a format supported by the given version of the program
     */
    public AutorizationTable(String path) throws URISyntaxException, IOException, FormatException{
      ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
      readLockFile = lock.readLock();
      writeLockFile = lock.writeLock();
      _InitPath(path);
    }
    /**
     * To register the user
     * @param login - login of user
     * @param password - user's password
     * @param role_id  - user's role in system
     * @return system identifier of registered user
     * @throws RegistrationFailed - if the parametes are null-valuen or empty string
     *  or user with the same login already registered, then generating exception of this type
     * @throws IOException - input-output error
     */
    public long Registration(String login, String password, long role_id) throws RegistrationFailed,IOException{
        if (login == null || login.equals("")) {
            throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
        }
        if (password == null || password.equals("")) {
            throw new RegistrationFailed(RegistrationFailed.PASSWORD_ERROR);
        }
        if (table.isKey(login)) {
            throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
        }

        LongComparable lID = new LongComparable(IDCounter++);
        LongComparable role = new LongComparable(role_id);
        RecordThreadTable record = new RecordThreadTable(FIELD_COUNT);
        String md5psw = _getPasswordMD5(password);
        if (md5psw == null) {
            md5psw = password;
        }
        record.set(md5psw, PASSWORD);
        record.set(lID, ID);
        record.set(role, ROLE);
        table.addItem(login, record);
        mapID.putIfAbsent(lID, login);
        size++;
        if (!_Export()) {
            throw new IOException(); //Export table into file
        }
        return lID.value;
    }

    /**
     * Removal of the accounting record of the user
     * @param ID - system identifier of user
     * @throws IOException - input-output error
     * @return false - In a case if the user with the set system identifier does not exist
     *         true - In a case if removal is successfully executed
     */
    public boolean unregistration(long ID) throws IOException {
        LongComparable holder = new LongComparable(ID);
        String login = mapID.get(holder);
        if (login == null) {
            return false;
        }
        mapID.remove(holder);
        table.removeItem(login);
        size--;
        if (!_Export()) {
            throw new IOException(); //Export table into file
        }
        return true;
    }

    /**
     * Carries out check, - whether the user in system is registered
     * @param login - a login of the user
     * @param Password - the password of the user
     * @return the system identifier of the user
     * @throws autorization. RegistrationFailed if the login or the password are specified incorrectly,
     * there is an exception of the given type
     */
     public long getID(String login, String Password) throws RegistrationFailed{
         if(Password == null) throw new RegistrationFailed(RegistrationFailed.PASSWORD_ERROR);
         String PasswordTemp = _getPasswordMD5(Password);
         if(PasswordTemp != null) Password = PasswordTemp;
         return _getID(login, Password);
     }

    public boolean isRegistration(String login, String Password){
       try{
        getID(login,Password);
        return true;
       }catch(RegistrationFailed e){
        return false;
       }
    }

    /**
     * remove all accounts~
     * @throws IOException - input-output error
     */
    public void clear() throws IOException{
     IDCounter = 0;
     size = 0;
     mapID.clear();
     table.clear();
     if(!_Export()) throw new IOException(); //Export table into file
    }

    /**
     * Allows to modify the registered login, on the active system identifier
     * @param ID - the system identifier
     * @param NewLogin - a new login
     * @return true - login replacement has gone right, false - in login change is given up
     * in a consequence of not registered system identifier
     * @throws autorization. RegistrationFailed - the login on which is made attempt to change
     * the old login is already registered in system. Or new login equal null value or empty string.
     * @throws IOException - input-output error
     */
    public boolean modifyLogin(long lID, String NewLogin) throws RegistrationFailed, IOException {
        LongComparable holder = new LongComparable(lID);
        String login = mapID.get(holder);
        if (login == null) {
            return false;
        }
        if (NewLogin == null || NewLogin.equals("")) {
            throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
        }
        if (mapID.containsKey(NewLogin)) {
            throw new RegistrationFailed(RegistrationFailed.LOGIN_ERROR);
        }
        mapID.replace(holder, NewLogin);
        RecordThreadTable record = table.get(login);
        table.removeItem(login);
        table.addItem(NewLogin, record);
        if (!_Export()) {
            throw new IOException(); //Export table into file
        }
        return true;
    }

    /**
     * Allows the user to change the own password
     * @param ID - the system identifier of the user
     * @param NewPassword - the new password
     * @return true - in case of successful performance of operation,
     * false - in case the user with the set system identifier
     * it is not registered in system
     * @throws autorization. RegistrationFailed - in case parametre NewPassword
     * it is equal to null-value or empty string
     * @throws IOException - input-output error
     */
    public boolean modifyPassword(long lID, String NewPassword) throws RegistrationFailed, IOException {
        if (NewPassword == null || NewPassword.equals("")) {
            throw new RegistrationFailed(RegistrationFailed.PASSWORD_ERROR);
        }
        LongComparable holder = new LongComparable(lID);
        String login = mapID.get(holder);
        if (login == null) {
            return false;
        }
        RecordThreadTable record = table.get(login);

        String md5psw = _getPasswordMD5(NewPassword);
        if (md5psw == null)
            md5psw = NewPassword;

        record.set(md5psw, PASSWORD);
        if (!_Export()) {
            throw new IOException(); //Export table into file
        }
        return true;
    }

     /**
     * To get the path to file of autorization table
     * @return path
     */
    public String getPath() {
      return path;
    }

    /**
    * Count of registered users
    * @return size
    */
    public int getSize() {
      return size;
    }

    /**
     * To import information from file
     * @param path - path to file contents autorization infromation.
     * @param typeImport Can trample down values of one of following constants of interface IAutorization:
     *        IMPORT_CURRENT or IMPORT_NEW.
     * @throws java.io.IOException input-output error
     * @throws java.io.FileNotFoundException entry file is not file
     * @throws autorization.FormatException The file format does not correspond to a format supported by the given version of the program
     */
    public void Import(String path, int typeImport) throws IOException,FileNotFoundException,FormatException {
        long lID;
        readLockFile.lock();
        ThreadTable<String> TempTable = new ThreadTable<String>();
        ConcurrentHashMap<LongComparable,String> mapIDTemp = new ConcurrentHashMap<LongComparable,String>();
        long IDCounterTemp = 0;


        DataInputStream fin = new DataInputStream(new FileInputStream(path));
        int nTotalSize = 0;
        try {
            if (!fin.readUTF().toString().equals(format)) {
                throw new FormatException();
            }

            switch (typeImport) {
                case IAutorization.IMPORT_NEW:
                    this.path = path;
                    break;
                case IAutorization.IMPORT_CURRENT:
                    break;
            }

            nTotalSize = fin.readInt();
            for (long i = 0; i < nTotalSize; i++) {
                lID = fin.readLong();
                if (lID > IDCounterTemp) IDCounterTemp = lID;

                String login = fin.readUTF();
                String password = fin.readUTF();
                long role_id = fin.readLong();

                if (TempTable.isKey(login))
                    throw new FormatException();
                LongComparable llID = new LongComparable(lID);
                LongComparable role = new LongComparable(role_id);
                RecordThreadTable record = new RecordThreadTable(FIELD_COUNT);       
                record.set(password, PASSWORD);
                record.set(llID, ID);
                record.set(role, ROLE);
                
                TempTable.addItem(login, record);
                if(mapIDTemp.putIfAbsent(llID,login) != null) throw new FormatException();;
            }
         } catch (EOFException e) {  
            throw new FormatException();
         }
         finally{
          fin.close();
          readLockFile.unlock();
         }
       table = TempTable;
       mapID = mapIDTemp;
       size = nTotalSize;
       IDCounter = IDCounterTemp;
    }



    /**
    * to Receive the iterator for the current table of authorisation
     * @return the interface on iterator for the current table of authorisation
     */
    public IAutorizationIterator getIterator(){
     return new AutorizationIterator();
    }

    //------------------Методы для работы с ролью пользователя------------------
    /**
     * Получить идентификатор роли пользователя
     * @param login - логин пользователя
     * @return идентификатор роли пользователя, -1 в случае если пользователь с таким логином не зарегистрирован в системе
     */
    public long getRole(String login) {
        if(!table.isKey(login)) return -1;
        RecordThreadTable rec = table.get(login);
        return ((LongComparable)rec.get(ROLE)).value;
    }

     /**
     * Получить идентификатор роли пользователя
     * @param ID - идентификатор пользователя
     * @return идентификатор роли пользователя
     */
    public long getRole(long ID) {
       String login = mapID.get(new LongComparable(ID));
       if(login == null) return -1;
       return getRole(login);
    }

    /**
     * Присвоить пользователя новую роль
     * @param login - логин
     * @param NewRoleID - идентификатор назначаемой роли
     * @return true - установка прошла удачно, false пользователя с таким логином не существует в системе
     */
    public boolean setRoleID(String login, long NewRoleID) {
       if(!table.isKey(login)) return false;
       RecordThreadTable rec = table.get(login);
       rec.set(new LongComparable(NewRoleID), ROLE);
       return true;
    }

    /**
     * Присвоить пользователя новую роль
     * @param ID - идентификатор поьзователя
     * @param NewRoleID - идентификатор назначаемой роли
     * @return true - установка прошла удачно, false пользователя с таким логином не существует в системе
    */
    public boolean setRoleID(long ID, long NewRoleID) {
       String login = mapID.get(new LongComparable(ID));
       if(login == null) return false;
       return setRoleID(login, NewRoleID);
    }
   //---------------------------------------------------------------------------


    //-------------------- Iterator class --------------------------------------
    public class AutorizationIterator implements IAutorizationIterator<String>{
        private ThreadTable.ThreadTableIterator iterator = table.getIterator();

        /**
         * set iterator on the collection begining
        */
        public void begin() {
          iterator.begin();
        }
        /**
         * To check up, whether it is possible to carry out transition to a following element
         * @return true - transition to carry out it is possible, false - transition to carry out it is impossible
        */
        public boolean hasNext() {
          return iterator.hasNext();
        }
       /**
         * Transition to a following element
         * @return true - transition is carried out, false - transition is not carried out
        */
        public boolean Next() {
          return iterator.Next();
        }
       /**
         * to Receive current record, in a case if record is not established
         * return null values
         * @return Record on which the element is established
        */
        public String getPassword() {
          return (String)iterator.getRecord().get(PASSWORD);
        }

        /**
         * Получить идентификатор роли записи
         * @return идентификатор роли
         */
        public long getRole(){
          return ((LongComparable)iterator.getRecord().get(ROLE)).value;
        }

         /**
         * to Receive a current key, with a case if the key is not established return null
         * values
         * @return the Login of the user on which is established iterator
        */
        public String getLogin() {
          return (String)iterator.getKey();
        }
        
       /**
        * Count of registered users
        * @return size
       */
        public int getSize() {
          return size;
        }
    }
    //-------------------------------------------------------------------------  
  //---------------------------------------------------------------------------
}


